package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
	"strings"

	"github.com/hyperledger/fabric-chaincode-go/shim"
	"github.com/hyperledger/fabric-protos-go/ledger/queryresult"
	"github.com/hyperledger/fabric-protos-go/peer"
)

type DSDBLogChaincode struct {
}

type DSDBLog struct {
	UserId           int    `json:"userId"`
	UserName         string `json:"userName"`
	OperationType    string `json:"operationType"`
	OperationContent string `json:"operationContent"`
	OperationResult  int    `json:"operationResult"`
	Time             int    `json:"time"`
}

var logStringFields = map[string]bool{"userName": true, "operationType": true, "operationContent": true}

func joinBrace(inner string) string {
	return "{" + inner + "}"
}

func joinSelect(inner string) string {
	return "\"selector\":" + joinBrace(inner)
}

func joinAnd(inner string) string {
	return "\"$and\":[" + inner + "]"
}

func joinSort(inner string) string {
	return "\"sort\":[" + inner + "]"
}

func paramString(key, value string, isValueQuoted bool) string {
	// int型不需要双引号括起来
	if isValueQuoted {
		return joinBrace(fmt.Sprintf(`"%s":"%s"`, key, value))
	} else {
		return joinBrace(fmt.Sprintf(`"%s":%s`, key, value))
	}
}

func parseArgs(args []string) (string, error) {
	// 前两个参数代表排序项和排序方法, asc/desc

	if len(args) < 2 {
		return "", errors.New("parseArgs with too little args")
	}

	sortField := args[0]
	sortOrder := args[1]

	if (len(args)-2)%3 != 0 {
		return "", errors.New("parseArgs with wrong number of args")
	}

	var quotedArgs []string
	for i := 2; i < len(args); i += 3 {

		key, value, argType := args[i], args[i+1], args[i+2]
		_, isStringValue := logStringFields[key]

		if isStringValue == false {
			if _, err := strconv.Atoi(value); err != nil {
				return "", errors.New(fmt.Sprintf("Parse arg error: %v", err))
			}
		}

		if args[i+2] == "" {
			quotedArgs = append(quotedArgs, paramString(key, value, isStringValue))
		} else {
			quotedArgs = append(quotedArgs, paramString(key, paramString("$"+argType, value, isStringValue), false))
		}
	}
	selectString := joinSelect(joinAnd(strings.Join(quotedArgs, ",")))

	if sortField == "" || sortOrder == "" {
		return joinBrace(selectString), nil
	} else {
		if sortOrder != "asc" {
			sortOrder = "desc"
		}
		sortString := joinSort(paramString(sortField, sortOrder, true))
		return joinBrace(selectString + "," + sortString), nil
	}
}

func MarshalError(err error) peer.Response {
	return shim.Error(fmt.Sprintf("Marshl error: %v", err))
}

func (t *DSDBLogChaincode) Init(_ shim.ChaincodeStubInterface) peer.Response {
	return shim.Success(nil)
}

func (t *DSDBLogChaincode) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
	function, args := stub.GetFunctionAndParameters()
	switch function {
	case "set":
		return t.Set(stub, args)
	case "query_by_string":
		return t.QueryByString(stub, args)
	case "query":
		return t.Query(stub, args)
	case "query_by_time_range":
		return t.QueryByTimeRange(stub, args)
	default:
		return shim.Error("Invalid function name")
	}
}

func (t *DSDBLogChaincode) Set(stub shim.ChaincodeStubInterface, args []string) peer.Response {
	if len(args) < 6 {
		return shim.Error(fmt.Sprintf("Incorrect number of args of Set"))
	}

	userId, err := strconv.Atoi(args[0])
	if err != nil {
		return shim.Error(fmt.Sprintf("Parse userId error: %v", err))
	}
	time, err := strconv.Atoi(args[5])
	if err != nil {
		return shim.Error(fmt.Sprintf("Parse time error: %v", err))
	}
	operationResult, err := strconv.Atoi(args[4])
	if err != nil {
		return shim.Error(fmt.Sprintf("Parse operationResult error: %v", err))
	}

	log := DSDBLog{
		UserId:           userId,
		UserName:         args[1],
		OperationType:    args[2],
		OperationContent: args[3],
		OperationResult:  operationResult,
		Time:             time,
	}
	if logJson, jsonErr := json.Marshal(log); jsonErr == nil {
		if peerErr := stub.PutState(stub.GetTxID(), logJson); peerErr != nil {
			return shim.Error(peerErr.Error())
		}
	} else {
		return MarshalError(jsonErr)
	}
	return shim.Success(nil)
}

func (t *DSDBLogChaincode) QueryByString(stub shim.ChaincodeStubInterface, args []string) peer.Response {
	if len(args) < 3 {
		return shim.Error(fmt.Sprintf("Incorrect number of args of Set"))
	}

	// 查询语句
	query := args[0]

	// 查询页数
	pageSizeStr := args[1]
	pageSize, err := strconv.Atoi(pageSizeStr)
	if err != nil {
		return shim.Error(fmt.Sprintf("Parse pageSize error: %v", err))
	}

	// 设置查询分页的最大最小值
	if pageSize <= 0 {
		pageSize = 10
	}
	if pageSize >= 50 {
		pageSize = 50
	}

	// 上次查询书签，加快查询
	bookmark := args[2]
	iter, _, err := stub.GetQueryResultWithPagination(query, int32(pageSize), bookmark)
	if err != nil {
		return shim.Error(fmt.Sprintf("GetQueryResultWithPagination error: %v", err))
	}
	var results []*queryresult.KV
	for iter.HasNext() {
		item, iterErr := iter.Next()
		if iterErr != nil {
			return shim.Error(fmt.Sprintf("Iter error: %v", err))
		}
		results = append(results, item)
	}
	res, err := json.Marshal(results)
	if err != nil {
		return MarshalError(err)
	}
	return shim.Success(res)
}

// Query 进行查询，前四个参数分别为:
// 1. 查询条数
// 2. 上次查询书签(默认空则是第一次查询)
// 3. 排序项(默认空)
// 4. 排序方式(asc/desc)
// 剩下参数量是3的倍数, 查询匹配项, 模式为: key(匹配键), value(匹配值), type(匹配类型, 默认空则是等值匹配)
// 其余可能用得上的匹配方式主要为 lte(小于)、gte(大于)、regex(正则匹配), https://docs.couchdb.org/en/stable/api/database/find.html
func (t *DSDBLogChaincode) Query(stub shim.ChaincodeStubInterface, args []string) peer.Response {
	if len(args) < 4 {
		return shim.Error(fmt.Sprintf("Incorrect number of args of Set"))
	}

	pageSizeStr := args[0]
	bookmark := args[1]
	quoteString, err := parseArgs(args[2:])
	if err != nil {
		return shim.Error(fmt.Sprintf("%v", err))
	}
	return t.QueryByString(stub, []string{quoteString, pageSizeStr, bookmark})
}

// QueryByTimeRange 按照时间戳范围进行查询，前6个参数分别是:
// 1. 查询条数
// 2. 上次查询书签(默认空则是第一次查询)
// 3. 开始时间戳
// 4. 结束时间戳
// 5. 排序项(默认空)
// 6. 排序方式(asc/desc)
func (t *DSDBLogChaincode) QueryByTimeRange(stub shim.ChaincodeStubInterface, args []string) peer.Response {
	if len(args) < 6 {
		return shim.Error(fmt.Sprintf("Incorrect number of args of Set"))
	}

	pageSizeStr := args[0]
	bookmark := args[1]
	args = append(args, []string{"time", args[2], "gte"}...)
	args = append(args, []string{"time", args[3], "lte"}...)
	quoteString, err := parseArgs(args[4:])
	if err != nil {
		return shim.Error(fmt.Sprintf("%v", err))
	}
	return t.QueryByString(stub, []string{quoteString, pageSizeStr, bookmark})
}

func main() {
	err := shim.Start(new(DSDBLogChaincode))
	if err != nil {
		fmt.Printf("Error starting DSDB chaincode: %s", err)
	}
}
